﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace MvvmInteractivity.Behaviours
{
	public class DragAndDropBehaviourBase<T> : Behavior<T>
		where T : FrameworkElement
	{
		// Using a DependencyProperty as the backing store for TargetDataContext. This enables animation, styling, binding, etc...
		public static readonly DependencyProperty TargetDataContextProperty =
			DependencyProperty.Register(
				"TargetDataContext",
				typeof(object),
				typeof(DragAndDropBehaviourBase<T>),
				new PropertyMetadata(OnTargetDataContextChanged));

		private Point lastMouseDown;
		private IDragAndDrop viewModel;

		public object TargetDataContext
		{
			get { return GetValue(TargetDataContextProperty); }
			set { SetValue(TargetDataContextProperty, value); }
		}

		/// <summary>
		/// Called after the behavior is attached to AssociatedObject
		/// </summary>
		protected override void OnAttached()
		{
			base.OnAttached();
			OnTargetDataContextChanged();
		}

		protected TViewModel GetViewModel<TViewModel>()
			where TViewModel : class , IDragAndDrop
		{
			return (TargetDataContext ?? AssociatedObject.DataContext) as TViewModel;
		}

		protected virtual void OnTargetDataContextChanged()
		{
			//When OnTargetDataContextChanged is called before OnAttached.
			if (AssociatedObject == null)
			{
				return;
			}

			viewModel = GetViewModel<IDragAndDrop>();

			if (viewModel == null)
			{
				return;
			}

			AssociatedObject.AllowDrop = true;
			AssociatedObject.PreviewDrop += OnAssociatedObjectDrop;
			AssociatedObject.DragOver += OnAssociatedObjectDragOver;

			AssociatedObject.PreviewDragEnter += OnAssociatedObjectPreviewDrag;
			AssociatedObject.PreviewDragOver += OnAssociatedObjectPreviewDrag;

			AssociatedObject.MouseMove += OnAssociatedObjectMouseMove;
			AssociatedObject.MouseDown += OnAssociatedObjectMouseDown;
		}

		private void OnAssociatedObjectPreviewDrag(object sender, DragEventArgs e)
		{
			//To make sure that drag will be processed correctly.
			e.Handled = true;
		}

		private static void OnTargetDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var behavior = d as DragAndDropBehaviourBase<T>;
			if (behavior != null)
			{
				behavior.OnTargetDataContextChanged();
			}
		}

		private void OnAssociatedObjectDragOver(object sender, DragEventArgs e)
		{
			if (e.Data == null || !e.Data.GetDataPresent(viewModel.DragFormat))
			{
				e.Effects = DragDropEffects.None;
			}
		}

		private void OnAssociatedObjectMouseDown(object sender, MouseButtonEventArgs e)
		{
			if (e.ChangedButton == MouseButton.Left)
			{
				lastMouseDown = e.GetPosition(AssociatedObject);
			}
		}

		private void OnAssociatedObjectMouseMove(object sender, MouseEventArgs e)
		{
			if (viewModel.IsDragAvailable == false)
			{
				return;
			}

			if (e.LeftButton != MouseButtonState.Pressed)
			{
				return;
			}

			var currentPosition = e.GetPosition(AssociatedObject);

			if ((Math.Abs(currentPosition.X - lastMouseDown.X) <= SystemParameters.MinimumHorizontalDragDistance) &&
				(Math.Abs(currentPosition.Y - lastMouseDown.Y) <= SystemParameters.MinimumVerticalDragDistance))
			{
				return;
			}

			var data = viewModel.DragData;
			var format = viewModel.DragFormat;

			if (data == null)
			{
				return;
			}

			var dataObject = new DataObject(format, data);
			DragDrop.DoDragDrop(AssociatedObject, dataObject, DragDropEffects.Copy);
		}

		protected virtual void OnAssociatedObjectDrop(object sender, DragEventArgs e)
		{
			if (e.Data == null)
			{
				return;
			}

			var supportedFormats = viewModel.DropFormats;
			var availableFormats = e.Data.GetFormats().ToList();

			if (supportedFormats != null)
			{
				foreach (var format in supportedFormats)
				{
					if (availableFormats.Contains(format))
					{
						var data = e.Data.GetData(format);
						var processed = viewModel.DoDrop(format, data);
						if (processed)
						{
							break;
						}
					}
				}
			}
		}
	}

}
